Imports Microsoft.VisualBasic
Imports System
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Reflection

Namespace ViewModel
    ''' <summary>
    ''' Base class for all ViewModel classes in the application.
    ''' It provides support for property change notifications 
    ''' and has a DisplayName property.  This class is abstract.
    ''' </summary>
    Public MustInherit Class ViewModelBase
        Implements INotifyPropertyChanged, IDisposable, ISerializable

#Region "Constructor"

        Protected Sub New()
        End Sub

#End Region ' Constructor

#Region "DisplayName"

        ''' <summary>
        ''' Returns the user-friendly name of this object.
        ''' Child classes can set this property to a new value,
        ''' or override it to determine the value on-demand.
        ''' </summary>
        Private privateDisplayName As String
        Public Overridable Property DisplayName() As String
            Get
                Return privateDisplayName
            End Get
            Protected Set(ByVal value As String)
                privateDisplayName = value
            End Set
        End Property

#End Region ' DisplayName

#Region "Debugging Aides"

        ''' <summary>
        ''' Warns the developer if this object does not have
        ''' a public property with the specified name. This 
        ''' method does not exist in a Release build.
        ''' </summary>
        <Conditional("DEBUG"), DebuggerStepThrough()> _
        Public Sub VerifyPropertyName(ByVal propertyName As String)
            ' Verify that the property name matches a real,  
            ' public, instance property on this object.
            If TypeDescriptor.GetProperties(Me)(propertyName) Is Nothing Then
                Dim msg As String = "Invalid property name: " & propertyName

                If Me.ThrowOnInvalidPropertyName Then
                    Throw New Exception(msg)
                Else
                    Debug.Fail(msg)
                End If
            End If
        End Sub

        ''' <summary>
        ''' Returns whether an exception is thrown, or if a Debug.Fail() is used
        ''' when an invalid property name is passed to the VerifyPropertyName method.
        ''' The default value is false, but subclasses used by unit tests might 
        ''' override this property's getter to return true.
        ''' </summary>
        Private privateThrowOnInvalidPropertyName As Boolean
        Protected Overridable Property ThrowOnInvalidPropertyName() As Boolean
            Get
                Return privateThrowOnInvalidPropertyName
            End Get
            Set(ByVal value As Boolean)
                privateThrowOnInvalidPropertyName = value
            End Set
        End Property

#End Region ' Debugging Aides

#Region "INotifyPropertyChanged Members"

        ''' <summary>
        ''' Raised when a property on this object has a new value.
        ''' </summary>
        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

        ''' <summary>
        ''' Raises this object's PropertyChanged event.
        ''' </summary>
        ''' <param name="propertyName">The property that has a new value.</param>
        Protected Overridable Sub OnPropertyChanged(ByVal propertyName As String)
            Me.VerifyPropertyName(propertyName)

            Dim handler As PropertyChangedEventHandler = Me.PropertyChangedEvent
            If handler IsNot Nothing Then
                Dim e = New PropertyChangedEventArgs(propertyName)
                handler(Me, e)
            End If
        End Sub

#End Region ' INotifyPropertyChanged Members

#Region "IDisposable Members"

        ''' <summary>
        ''' Invoked when this object is being removed from the application
        ''' and will be subject to garbage collection.
        ''' </summary>
        Public Sub Dispose() Implements IDisposable.Dispose
            Me.OnDispose()
            GC.SuppressFinalize(Me)
        End Sub

        ''' <summary>
        ''' Child classes can override this method to perform 
        ''' clean-up logic, such as removing event handlers.
        ''' </summary>
        Protected Overridable Sub OnDispose()
        End Sub

        '#If DEBUG Then
        '        ''' <summary>
        '        ''' Useful for ensuring that ViewModel objects are properly garbage collected.
        '        ''' </summary>
        '        Protected Overrides Sub Finalize()
        '            Dim msg As String = String.Format("{0} ({1}) ({2}) Finalized", Me.GetType().Name, Me.DisplayName, Me.GetHashCode())
        '            System.Diagnostics.Debug.WriteLine(msg)
        '        End Sub
        '#End If

#End Region ' IDisposable Members

        Public Function Serialize(Optional ByVal name As String = "") As System.Xml.Linq.XElement Implements ISerializable.Serialize
            If name = "" Then
                name = Me.GetType.Name
            End If

            Dim rootElement = New XElement(name)

            For Each propertyInfo In Me.GetType.GetProperties()

                If Not propertyInfo.CanWrite AndAlso _
                   propertyInfo.PropertyType.IsGenericType AndAlso _
                   propertyInfo.PropertyType.FullName.StartsWith("System.Collections.ObjectModel.ObservableCollection") Then
                    ' ReadOnly ObservableCollection Property

                    Dim collectionElement = New XElement(propertyInfo.Name)

                    For Each item In DirectCast(propertyInfo.GetValue(Me, Nothing), IList)
                        If item.GetType.IsClass AndAlso _
                           Not item.GetType Is GetType(String) Then
                            Dim isSerializable = False
                            Dim itemType = item.GetType
                            Do While Not itemType.BaseType Is GetType(Object)
                                If itemType.BaseType Is GetType(ISerializable) Then
                                    isSerializable = True
                                    Exit Do
                                End If
                                itemType = itemType.BaseType
                            Loop

                            If isSerializable Then
                                Dim elements = DirectCast(itemType.InvokeMember("Serialize", BindingFlags.InvokeMethod, Nothing, item, New Object() {""}), XElement)
                                collectionElement.Add(elements)
                            End If
                        Else
                            Dim element = New XElement(item.GetType.Name)
                            element.Add(item)
                            collectionElement.Add(element)
                        End If
                    Next

                    rootElement.Add(collectionElement)

                ElseIf propertyInfo.CanWrite Then

                    If Not propertyInfo.PropertyType.IsClass OrElse _
                       propertyInfo.PropertyType Is GetType(String) Then
                        Dim element = New XElement(propertyInfo.Name)
                        element.Add(propertyInfo.GetValue(Me, Nothing))
                        rootElement.Add(element)

                    ElseIf propertyInfo.PropertyType Is GetType(Uri) Then
                        Dim element = New XElement(propertyInfo.Name)
                        element.Add(propertyInfo.GetValue(Me, Nothing))
                        rootElement.Add(element)
                    Else

                        Dim isSerializable = False
                        Dim itemType = propertyInfo.PropertyType
                        Do While Not itemType.BaseType Is GetType(Object)
                            If itemType.BaseType Is GetType(ISerializable) Then
                                isSerializable = True
                                Exit Do
                            End If
                            itemType = itemType.BaseType
                        Loop

                        If isSerializable Then
                            Dim elements = DirectCast(itemType.InvokeMember("Serialize", BindingFlags.InvokeMethod, Nothing, propertyInfo.GetValue(Me, Nothing), New Object() {propertyInfo.Name}), XElement)
                            rootElement.Add(elements)

                        End If

                    End If
                End If
            Next

            Return rootElement
        End Function

        Public Sub Deserialize(ByVal rootElement As System.Xml.Linq.XElement) Implements ISerializable.Deserialize
            For Each propertyInfo In Me.GetType.GetProperties()

                If Not propertyInfo.CanWrite AndAlso _
                   propertyInfo.PropertyType.IsGenericType AndAlso _
                   propertyInfo.PropertyType.FullName.StartsWith("System.Collections.ObjectModel.ObservableCollection") Then
                    ' ReadOnly ObservableCollection Property

                    Dim list = DirectCast(propertyInfo.GetValue(Me, Nothing), IList)

                    Dim argType = propertyInfo.PropertyType.GetGenericArguments.First

                    Dim isDeserializable = False
                    Dim itemType = argType
                    Do While Not itemType.BaseType Is GetType(Object)
                        If itemType.BaseType Is GetType(ISerializable) Then
                            isDeserializable = True
                            Exit Do
                        End If
                        itemType = itemType.BaseType
                    Loop


                    Dim listName = propertyInfo.Name
                    Dim listElements = From el In rootElement.Elements Where el.Name = listName

                    If listElements.Count = 0 Then
                        Exit For
                    End If

                    Dim itemName = argType.Name
                    Dim elements = From el In listElements.First.Elements Where el.Name = itemName

                    If isDeserializable Then
                        For Each el In elements
                            Dim deserializableClass = Activator.CreateInstance(argType)
                            argType.InvokeMember("Deserialize", BindingFlags.InvokeMethod, Nothing, deserializableClass, New Object() {el})
                            list.Add(deserializableClass)
                        Next
                    Else
                        For Each el In elements
                            If argType Is GetType(String) Then
                                list.Add(el.Value)
                            End If
                        Next
                    End If

                ElseIf propertyInfo.CanWrite Then

                    If Not propertyInfo.PropertyType.IsClass OrElse _
                       propertyInfo.PropertyType Is GetType(String) Then

                        Dim name = propertyInfo.Name
                        Dim elements = From el In rootElement.Elements Where el.Name = name
                        If elements.Count = 1 Then

                            Dim obj As Object = Nothing
                            If propertyInfo.PropertyType Is GetType(Boolean) Then
                                obj = CBool(elements.Value)
                            ElseIf propertyInfo.PropertyType Is GetType(DateTime) Then
                                obj = CDate(elements.Value)
                            ElseIf propertyInfo.PropertyType Is GetType(String) Then
                                obj = elements.Value
                            ElseIf propertyInfo.PropertyType Is GetType(Integer) Then
                                obj = CInt(elements.Value)
                            ElseIf propertyInfo.PropertyType Is GetType(Double) Then
                                obj = CDbl(elements.Value)
                            End If

                            propertyInfo.SetValue(Me, obj, BindingFlags.SetProperty, Nothing, Nothing, Globalization.CultureInfo.CurrentCulture)

                        End If

                    ElseIf propertyInfo.PropertyType Is GetType(Uri) Then

                        Dim name = propertyInfo.Name
                        Dim elements = From el In rootElement.Elements Where el.Name = name
                        If elements.Count = 1 Then
                            propertyInfo.SetValue(Me, New Uri(elements.Value), BindingFlags.SetProperty, Nothing, Nothing, Globalization.CultureInfo.CurrentCulture)
                        End If
                    Else

                        Dim name = propertyInfo.Name
                        Dim elements = From el In rootElement.Elements Where el.Name = name

                        If elements.Count > 0 Then
                            Dim deserializableClass = Activator.CreateInstance(propertyInfo.PropertyType)
                            propertyInfo.PropertyType.InvokeMember("Deserialize", BindingFlags.InvokeMethod, Nothing, deserializableClass, New Object() {elements.First})
                            propertyInfo.SetValue(Me, deserializableClass, BindingFlags.SetProperty, Nothing, Nothing, Globalization.CultureInfo.CurrentCulture)
                        End If

                    End If
                End If
            Next
        End Sub

    End Class

End Namespace